Contents
  1. 1. 前言
  2. 2. 关于 RegularJS 开发的一些槽点
    1. 2.1. 1. 过度庞大的组件
      1. 2.1.1. 描述
      2. 2.1.2. 建议
    2. 2.2. 2. 组件原型上存放一些原本可以抽出的方法
      1. 2.2.1. 描述
      2. 2.2.2. 建议
    3. 2.3. 3. 滥用双向绑定
      1. 2.3.1. 描述
      2. 2.3.2. 解决
    4. 2.4. 4. 习惯把所有数据都放置到 this.data 上
      1. 2.4.1. 描述
      2. 2.4.2. 解决
    5. 2.5. 5. 组件传入的参数结构不明了
      1. 2.5.1. 描述
      2. 2.5.2. 解决
    6. 2.6. 6. 较长的表达式
      1. 2.6.1. 描述
      2. 2.6.2. 解决
  3. 3. 总结

前言

近几年来,前端开发这个概念发生了明显的变化。多年以前,前端开发的重点可能是使用 HTML/CSS/JS 和一些兼容知识来完成页面的构建,动效的制作。

而现阶段的前端开发,工作的很重要的部分,就是使用 Vue/React 或其他 MVVM 框架来完成我们的组件,并组织我们的页面。

我们来进入现实的开发场景。你接手一个需求后,最先做的是什么?

如果是一个新项目,你可能需要思考一些架构上的东西,比如:是否启用单页架构、使用什么开发框架、是否需要状态管理工具、资源构建怎么处理。嗯,你会饶有乐趣,然后兴致满满地开工。

事实上,新的项目并不多,我们更多时候也都是在维护现有代码,可能有很多事情会让你你觉得无奈的。比如,你对现有架构的不满意,对当前的业务逻辑书写得这么复杂表示不理解。于是,你想要重构。当然,我也希望重构。但是,我们的生命是否允许你消耗这段时间来重构这段老的逻辑。答案,是否。

所以,更重要地就是,阻止更多的代码恶化,这似乎比你单枪匹马搞重构,有用的多。这也是本文的重点,如何在现有项目,新需求的场景下,书写更可维护的组件化代码。

关于 RegularJS 开发的一些槽点

RegularJS 是网易出品的一款 MVVM 框架,本身其设计思路,和学习成本,都算中上。但是,作者对于设计思路在文档中的体现太少,导致顺势而为的使用者不多。比如说,下面的一些用法:

1. 过度庞大的组件

描述

一个组件,原本可以拆成若干个组件来单独处理 UI 和 行为,现在被放到了一个组件里,使得组件过大,导致维护性、扩展性降低。

建议

组件拆分有很多原则可以参考:

  1. 六大设计原则的 「单一职责原则」,尽可能保证每个组件的职责单一性;
  2. 当前比较盛行的 「容器组件」与「UI 组件」的方案,「容器组件」用于控制用户状态,并做一些数据更新操作,不直接表现UI;「UI组件」则主要负责 UI 展现,不做具体的逻辑处理,需要更新数据,尽可能使用事件,交给容器组件处理;

2. 组件原型上存放一些原本可以抽出的方法

描述

组件原型链上放置了过多不属于 this 的处理逻辑,导致组件的可维护性与可理解性降低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const ComponentA = Regular.extend({
getList() {
this.$request({
url,
success({
code, body
}) {
if (code === 200) {
this.getListCallBack();
}
}
})
},
getListCallBack({list}) {
this.filterList(list);
},
filterList(list) {
this.data.list = list.map(({title, content}) => {
return {
title, content
};
});
}
});

建议

纯函数对于可测试性和可预期性都有较大的优势。

所以,不如把一些无关 this 的方法抽出来吧,把他们作为一些纯函数在你的组件生命周期中调用。

比如,把发 Ajax 请求的方法抽成 service,统一管理,把一些对象处理函数独立成 filter,总之 this 上尽可能的干净,对后期的维护是很有帮助的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const filterList(list) {
return list.map(({title, content}) => {
return {
title, content
};
});
}
const ComponentA = Regular.extend({
computed: {
title(){
this.getTitles();
}
},
onButtonClick() {
service.getList({
}).then(() => {
this.setList(list);
})
},
setList({list}) {
this.data.list = filterList(list);
},
getTitles() {
return this.data.list.map(item => item.title);
}
});

一般三类方法是必须的 1. 事件 handler; 2. compute get 方法; 3. data 的操作方法

  1. 事件 handler 可以让你的函数更贴近于事件编程方式,更便于理解;
  2. 使用 set 方法去设置 data 的数据,可以让你的 data 修改更可控;
  3. 将 computed 抽出成方法,可以在 js 中复用 computed 的逻辑;

原则就是,能挪出去的就挪出去作为纯函数。

3. 滥用双向绑定

描述

双向绑定,是指数据更新与UI行为的绑定操作。经常会出现一个对象依次传入多层组件的情况,这时候最省力的数据更新方式,可能就是直接在子组件内修改这个对象。

Index 组件

1
<ComponentA obj={obj}></ComponentA>

ComponentA 组件

1
<ComponentB obj={obj}></ComponentB>

ComponentB 组件

1
<ComponentC obj={obj}></ComponentC>

ComponentC 组件

1
<input r-model={obj.a}/>

我们看到如上这种情况,看似很巧妙的结合 RegularJS 的双向绑定,做了数据更新的操作,也能让最外层组件可以使用最新的 obj 对象做一些事情。但是,对于最外层组件而言, obj 的变化是不可预期的。换言之,开发者很难通过直接查看最外层组件,感知到 obj 对象可能发生的改变。这也是,双向绑定带来的弊端。

解决

双向绑定的使用,非常方便,在深层嵌套的组件数据传递上,尤为明显。但它会使得数据的改变源头难以追踪,代码接手时也容易留坑,不好理解。

那么,我们应该控制住自己,不要过分依赖这种能力。换句话说,如果你肯定你的项目是不需要被维护的,那么你去使用吧(逃)

一般场景下,单向数据流的启用方式,是通过事件。

ComponentA

1
2
3
4
<ComponentB
obj={obj}
on-btnClick={}
></ComponentB>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Regular.extend({
config() {
extend(this.data, {
obj: {
clicked: false
}
})
},
onComponentBBtnClick() {
this.setClicked();
}
setClicked() {
this.data.obj.clicked = true;
}
})

ComponentB

1
<button disable={clicked} on-click={this.$emit('btnClick')}>点我!</button>

那么,如何解决深层嵌套组件的数据更新问题呢,就是使用状态管理工具,后面会提到。

4. 习惯把所有数据都放置到 this.data

描述

习惯把所有数据都挂载到 this.data 上。

如果我们正视 data 的职责,data 只负责放置 UI 渲染相关数据。那么,我们平时开发中,很多操作是存在争议的:

  1. 请求获取下来的数据,直接放置到 this.data 上的方式,后端在提供在前端的数据中,难免会存在一些冗余的字段,会直接赋值到 data 上,最直接的影响是导致脏值检查耗时增加,这些字段的存在会让你的 data 显得凌乱不清晰;
  2. 大家可能会将一些逻辑计算的数据也放置到 data 上,但是这些数据对于模板渲染也是无关的。

解决

这个问题的关键,是正确地认知 this.data。对于 MVVM 框架而言,最大的亮点莫过于视图与数据的绑定(单向 or 双向), this.data 他真正的职责就是作为视图层渲染时提供必要的数据。

那么,你可能就能理解一些类似的话了:
「React只是一个优秀的视图层框架,他将复杂的状态管理留给了你」

所以,原则上不该把所有数据都放置到 data 上。其次,可以根据 1 中提到的方案,使用 「容器组件」和「UI组件」的分类,来区分我们的组件状态。

  • 「容器组件」可以放置一些逻辑处理相关的字段,用于与后端交互和分发组件。
  • 「UI 组件」的 data 应该避免赋值一些视图无关的状态。

当然,必要时,推荐引入状态管理工具来管理你的状态。

5. 组件传入的参数结构不明了

描述

Index

1
<ComponentA obj={obj}></ComponentA>

组件 ComponentA 的传入参数是 obj,但是 obj 对于调用者来说,是一个黑盒。直接阅读该组件的例子或现有代码,无法直接地了解到该组件的使用姿势。

解决

所以,尽可能地直接明了地展现组件所需的属性,同时需要遵守遵守程序设计的「迪米特法则(最少知道原则)」,让所需的属性尽可能的少。

1
2
3
4
<ComponentA
name={name}
age={age}
></ComponentA>

6. 较长的表达式

描述

Index

1
2
3
{#list coupons as coupon}
使用条件:{coupon.couponType === 1 ? `满¥ ${coupon.threshold} 使用` : '无条件使用'}
{/list}

解决

模板中不该存在过多的计算逻辑,很重要一点是,我们一般无法对模板进行 debug,所以将逻辑抽出安放到 filter 或 computed 中,是个不错的方式。

1
2
3
4
5
6
7
8
9
10
11
Regular.extend({
template: `
{#list coupons as coupon}
使用条件:{coupon|getCouponCondition}
{/list}
`,
}).filter({
getCouponCondition(coupon) {
return coupon.couponType === 1 ? `满¥ ${coupon.threshold} 使用` : '无条件使用';
}
})

总结

篇幅原因,简单列举几条。

书写可维护的 Regular 组件,需要我们有关于设计基本原则的一些理解,并在实际的 MVVM 框架下展开。

有接触过 Redux 的同学,可能会发现,上述的一些解决方案,有很多是借鉴自 Redux 的,如 单向数据流 、「容器组件」与「UI 组件」。那么如何使用状态管理来更为优雅地解决这些问题,本人另一篇文章 Redux基础与实践 正在飞奔而来!!!

Contents
  1. 1. 前言
  2. 2. 关于 RegularJS 开发的一些槽点
    1. 2.1. 1. 过度庞大的组件
      1. 2.1.1. 描述
      2. 2.1.2. 建议
    2. 2.2. 2. 组件原型上存放一些原本可以抽出的方法
      1. 2.2.1. 描述
      2. 2.2.2. 建议
    3. 2.3. 3. 滥用双向绑定
      1. 2.3.1. 描述
      2. 2.3.2. 解决
    4. 2.4. 4. 习惯把所有数据都放置到 this.data 上
      1. 2.4.1. 描述
      2. 2.4.2. 解决
    5. 2.5. 5. 组件传入的参数结构不明了
      1. 2.5.1. 描述
      2. 2.5.2. 解决
    6. 2.6. 6. 较长的表达式
      1. 2.6.1. 描述
      2. 2.6.2. 解决
  3. 3. 总结